from ast import literal_eval
import numpy as np
import copy
import yaml
basestring = (str, str)


class AttrDict(dict):

    def __getattr__(self, name):
        if name in self.__dict__:
            return self.__dict__[name]
        elif name in self:
            return self[name]
        else:
            raise AttributeError(name)

    def __setattr__(self, name, value):
        if name in self.__dict__:
            self.__dict__[name] = value
        else:
            self[name] = value
            
def load_yaml_to_AttrDict(filename):
    with open(filename, 'r') as f:
        cfg = AttrDict(yaml.load(f, Loader=yaml.FullLoader))
    return cfg

def merge_cfg_into_cfg(new_cfg_filename, old_cfg_filename):
    """Load a yaml config file and merge it into the global yaml config file."""
    with open(new_cfg_filename, 'r') as f_1:
        new_cfg = AttrDict(yaml.load(f_1, Loader=yaml.FullLoader))
    with open(old_cfg_filename, 'r') as f_2:
        old_cfg = AttrDict(yaml.load(f_2, Loader=yaml.FullLoader))
    _merge_a_into_b(new_cfg, old_cfg)
    return old_cfg


def _merge_a_into_b(a, b, stack=None):

    for k, v_ in a.items():
        full_key = '.'.join(stack) + '.' + k if stack is not None else k
        # a must specify keys that are in b
        if k not in b:
            raise KeyError('Non-existent config key: {}'.format(full_key))

        v = copy.deepcopy(v_)
        v = _decode_cfg_value(v)
        v = _check_and_coerce_cfg_value_type(v, b[k], k, full_key)

        # Recursively merge dicts
        if isinstance(v, AttrDict):
            try:
                stack_push = [k] if stack is None else stack + [k]
                _merge_a_into_b(v, b[k], stack=stack_push)
            except BaseException:
                raise
        else:
            b[k] = v


def _decode_cfg_value(v):
    if isinstance(v, dict):
        return AttrDict(v)
    # All remaining processing is only applied to strings
    if not isinstance(v, basestring):
        return v
    if isinstance(v, str):
        return v
    # Try to interpret `v` as a:
    #   string, number, tuple, list, dict, boolean, or None
    try:
        v = literal_eval(v)
    except ValueError:
        pass
    except SyntaxError:
        pass
    return v


def _check_and_coerce_cfg_value_type(value_a, value_b, key, full_key):
    type_b = type(value_b)
    type_a = type(value_a)
    if type_a is type_b:
        return value_a

    # Exceptions: numpy arrays, strings, tuple<->list
    if isinstance(value_b, np.ndarray):
        value_a = np.array(value_a, dtype=value_b.dtype)
    elif isinstance(value_b, basestring):
        value_a = str(value_a)
    elif isinstance(value_a, tuple) and isinstance(value_b, list):
        value_a = list(value_a)
    elif isinstance(value_a, list) and isinstance(value_b, tuple):
        value_a = tuple(value_a)
    else:
        raise ValueError(
            'Type mismatch ({} vs. {}) with values ({} vs. {}) for config '
            'key: {}'.format(type_b, type_a, value_b, value_a, full_key)
        )
    return value_a

# for test
#merge_cfg_into_cfg('/data/home/v-wewei/philly/experiments/azuer/scratch/resnet50/res50_scratch_8x8_s_3x1_0011_f_3x1_1111_ep256_lrcos_zip_sf.yaml', '/data/home/v-wewei/philly/experiments/base_config.yaml')
